package net.sf.odinms.exttools.doclet.pages;

import java.io.IOException;
import java.util.Arrays;
import com.sun.javadoc.*;
import net.sf.odinms.exttools.doclet.AbstractPageWriter;
import net.sf.odinms.exttools.doclet.Doclet;

/**
 * Creates "serialized-form.html" which contains serialization details.
 */
@SuppressWarnings("unchecked")
public final class SerializedForm extends AbstractPageWriter {
	
	/**
	 * The current package being documented.
	 */
	private PackageDoc currentPackage;
	
	/**
	 * Generate page detailing serialized forms of serial elements.
	 */
	public static void generateSerializedForm() {
		// It would seem wisest to just print "No serialized..."
		// if (! serialClassFoundToDocument(CONF.root.classes()))
		// return;
		String filename = "serialized-form" + CONF.ext;
		try {
			new SerializedForm(filename);
		}
		catch (IOException exc) {
			throw Doclet.exception(exc, filename);
		}
	}
	
	/**
	 * Initialize and create page detailing all serialized forms in project.
	 * 
	 * @param filename The desired name of the file (with extension).
	 * @throws IOException If {@link java.io.FileOutputStream} creation fails.
	 */
	private SerializedForm(String filename) throws IOException {
		super(filename);
		pageType = PageType.SERIALIZED;
		
		windowTitle =
				(CONF.windowtitle.length() > 0)
						? CONF.windowtitle
						: CONF.propertyText("Serialized_Form");
		
		printXhtmlHeader();
		println(open("h1") + CONF.propertyText("Serialized_Form") + close("h1"));
		for (PackageDoc pkg : CONF.root.specifiedPackages()) {
			currentPackage = pkg;
			buildPackageSerializedForm();
		}
		printXhtmlFooter();
		this.close();
	}
	
	/**
	 * Build the package serialized form for the current package being
	 * processed.
	 */
	private void buildPackageSerializedForm() {
		// TODO Construct list from "elements", not just this package's classes.
		// (Had a parameter "List elements", but no idea what it was for...)
		ClassDoc[] classes = currentPackage.allClasses(false);
		
		if (classes == null || classes.length == 0
				|| !serialInclude(currentPackage)
				|| !serialClassFoundToDocument(classes)) {
			return;
		}
		// build(elements);
		
		println(open("h2")
				+ CONF.propertyText("Package")
				+ " "
				+ linkToLabelHref(currentPackage.name(),
						hrefToDoc(currentPackage) + "package-summary"
								+ CONF.ext) + close("h2"));
		Arrays.sort(classes);
		for (ClassDoc currentClass : classes) {
			if (serialInclude(currentClass)) {
				String classHref = hrefToDoc(currentClass);
				
				String classLink =
						linkToLabelHrefTargetTitle(currentClass.name(),
								classHref, null, classFlavor(currentClass,
										false)
										+ " in package "
										+ currentClass.containingPackage());
				print(open("h3") + classLink);
				ClassDoc superclass = currentClass.superclass();
				if (superclass != null) {
					print(" extends "
							+ linkToLabelHrefTargetTitle(superclass.name(),
									hrefToDoc(superclass), null, classFlavor(
											superclass, false)
											+ " in package "
											+ superclass.containingPackage()));
				}
				println(close("h3"));
				
				String serialVersionUID = getSerialVersionUID(currentClass);
				if (serialVersionUID != null)
					println(open("p")
							+ "<code>static final long <strong>serialVersionUID</strong></code>: "
							+ serialVersionUID + close("p"));
				
				// TODO Print type parameters for field/return types
				
				MethodDoc[] methods = currentClass.serializationMethods();
				// Arrays.sort(methods);
				if (methods.length > 0)
					println(open("h4")
							+ CONF.propertyText("Serialization_Methods")
							+ close("h4"));
				for (MethodDoc method : methods)
					println(open("p") + method + close("p"));
				
				FieldDoc[] fields = currentClass.serializableFields();
				// Arrays.sort(fields);
				if (fields.length > 0) {
					println(open("h4") + CONF.propertyText("Serialized_Fields")
							+ close("h4"));
					println(open("dl"));
					for (FieldDoc field : fields) {
						String typeLink = field.type().typeName();
						if (!field.type().isPrimitive())
							typeLink =
									linkToLabelHref(
											typeLink,
											hrefToDoc(field.type().asClassDoc()));
						String fieldName = field.name();
						printDT(typeLink
								+ " "
								+ open("strong")
								+ linkToLabelHref(fieldName, classHref + "#"
										+ fieldName) + close("strong"));
						printDD(getCommentSummary(field));
					}
					println(close("dl"));
				}
			}
		}
	}
	
	/**
	 * Build the serial UID information for the given class, if it exists.
	 * 
	 * @param currentClass The class to check for a serial UID.
	 * @return A string with the serial UID contained in the class. If it
	 *         doesn't exists, returns null
	 */
	private String getSerialVersionUID(ClassDoc currentClass) {
		FieldDoc[] fields = currentClass.fields(false);
		for (int i = 0; i < fields.length; i++) {
			if (fields[i].name().equals("serialVersionUID")
					&& fields[i].constantValueExpression() != null) {
				return fields[i].constantValueExpression();
			}
		}
		return null;
	}
	
	/**
	 * Whether the given Doc should be included in the serialized form.
	 * 
	 * @param doc The Doc object to check for serializability.
	 * @return True if it should be included in the serialized form, else false.
	 */
	private static boolean serialInclude(Doc doc) {
		if (doc == null) {
			return false;
		}
		return doc.isClass()
				? serialClassInclude((ClassDoc) doc)
				: serialDocInclude(doc);
	}
	
	/**
	 * Do any of the given classes have a {@literal &#64;serialinclude} tag?
	 * 
	 * @param classes the classes to check.
	 * @return true if any of the given classes have a &#64;serialinclude tag.
	 */
	private static boolean serialClassFoundToDocument(ClassDoc[] classes) {
		for (int i = 0; i < classes.length; i++) {
			if (serialClassInclude(classes[i])) {
				return true;
			}
		}
		return false;
	}
	
	/**
	 * Whether the given ClassDoc should be included in the serialized form.
	 * 
	 * @param cd the ClassDoc object to check for serializability.
	 * @return True if it should be included in the serialized form, else false.
	 */
	// TODO Figure out how the call to ClassDoc.superclass[Type]() relates.
	private static boolean serialClassInclude(ClassDoc cd) {
		if (!cd.isEnum() && cd.superclass() != null && cd.isSerializable()) {
			if (cd.tags("serial").length > 0) {
				return serialDocInclude(cd);
			} else if (cd.isPublic() || cd.isProtected()) {
				return true;
			}
		}
		return false;
	}
	
	/**
	 * Whether the given Doc should be included in the serialized form.
	 * 
	 * @param doc the Doc object to check for serializability.
	 * @return True if it should be included in the serialized form, else false.
	 */
	private static boolean serialDocInclude(Doc doc) {
		if (doc.isEnum()) {
			return false;
		}
		Tag[] serial = doc.tags("serial");
		if (serial.length > 0) {
			String serialtext = serial[0].text().toLowerCase();
			if (serialtext.indexOf("exclude") >= 0)
				return false;
		}
		return true;
	}
	
}
